"""Qt Viewer."""

from __future__ import annotations

import functools
import logging
import math
import os
import sys

import moderngl as mgl
from PySide6.QtCore import QEvent, QSettings, QSize, Qt, Signal
from PySide6.QtGui import (
    QAction,
    QActionGroup,
    QMouseEvent,
    QNativeGestureEvent,
    QScreen,
    QSurfaceFormat,
    QWheelEvent,
)
from PySide6.QtOpenGLWidgets import QOpenGLWidget
from PySide6.QtWidgets import (
    QApplication,
    QHBoxLayout,
    QLabel,
    QMenu,
    QSizePolicy,
    QToolBar,
    QToolButton,
    QVBoxLayout,
    QWidget,
)

import vpype as vp

from .._scales import UnitType
from ..engine import Engine, ViewMode
from .utils import PenOpacityActionGroup, PenWidthActionGroup, load_icon, set_sigint_handler

__all__ = ["QtViewerWidget", "QtViewer", "show"]


_DEBUG_ENABLED = "VPYPE_VIEWER_DEBUG" in os.environ


# handle UI scaling
def _configure_ui_scaling():
    viewer_config = vp.config_manager.config.get("viewer", {})
    if "QT_SCALE_FACTOR" not in os.environ and "ui_scale_factor" in viewer_config:
        os.environ["QT_SCALE_FACTOR"] = str(viewer_config["ui_scale_factor"])


_configure_ui_scaling()

# set default format
default_format = QSurfaceFormat()
default_format.setVersion(3, 3)
default_format.setProfile(QSurfaceFormat.OpenGLContextProfile.CoreProfile)
default_format.setSamples(4)
QSurfaceFormat.setDefaultFormat(default_format)


class QtViewerWidget(QOpenGLWidget):
    """QGLWidget wrapper around :class:`Engine` to display a :class:`vpype.Document` in
    Qt GUI.
    """

    mouse_coords = Signal(str)

    def __init__(self, document: vp.Document | None = None, parent=None):
        """Constructor.

        Args:
            document: the document to display
            parent: QWidget parent
        """

        super().__init__(parent=parent)

        self.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding)

        self.setMouseTracking(True)

        self._document = document

        self._last_mouse_x = 0.0
        self._last_mouse_y = 0.0
        self._mouse_drag = False
        self._factor = 1.0

        # deferred initialization in initializeGL()
        self._ctx: mgl.Context | None = None
        self._framebuffer = None
        self._inited = False
        self.engine = Engine(
            view_mode=ViewMode.OUTLINE, show_pen_up=False, render_cb=self.update
        )

        # print diagnostic information
        screen = self.screen()
        logging.info(
            f"QScreen info: pixelRatio={screen.devicePixelRatio()}, "
            f"physicalSize={screen.physicalSize().toTuple()}, "
            f"physicalDotsPerInch={screen.physicalDotsPerInch()}, "
            f"virtualSize={screen.virtualSize().toTuple()}, "
            f"size={screen.size().toTuple()}, "
            f"logicalDotsPerInch={screen.logicalDotsPerInch()}, "
        )

    def document(self) -> vp.Document | None:
        """Return the :class:`vpype.Document` currently assigned to the widget."""
        return self._document

    def set_document(self, document: vp.Document | None) -> None:
        """Assign a new :class:`vpype.Document` to the widget."""
        self._document = document
        self.engine.document = document

    def on_screen_changed(self, screen: QScreen):
        self._factor = screen.devicePixelRatio()
        self.engine.pixel_factor = self._factor

        # force an update and reset of viewport's dimensions
        self.resizeGL(round(self.geometry().width()), round(self.geometry().height()))

    def _initializeGL(self):
        """Initialize ModernGL context.

        This used to be an override of QGLWidget.initializeGL() with PySide2. However, as per
        PySide6 documentation, "the framebuffer is not yet available at this stage". We thus
        defer the initialization to the first call of QOpenGLWidget.paintGL().
        """

        # this fails in __init__() (windowHandle() returns None)
        self.window().windowHandle().screenChanged.connect(self.on_screen_changed)

        self._ctx = mgl.create_context()
        logging.info(f"Context info: {self._ctx.info}")

        self._framebuffer = self._ctx.detect_framebuffer()
        self._framebuffer.viewport = (
            0,
            0,
            int(self._factor * self.width()),
            int(self._factor * self.height()),
        )

        self.engine.post_init(
            self._ctx, int(self._factor * self.width()), int(self._factor * self.height())
        )
        self.on_screen_changed(self.screen())
        self.engine.document = self._document
        self.engine.fit_to_viewport()

    def paintGL(self):
        if not self._inited:
            self._initializeGL()
            self._inited = True

        self._framebuffer.use()
        self.engine.render()

    def resizeGL(self, w: int, h: int) -> None:
        width = int(w * self._factor)
        height = int(h * self._factor)
        self.engine.resize(width, height)

        if self._framebuffer:
            self._framebuffer.viewport = (0, 0, width, height)

    def mousePressEvent(self, evt: QMouseEvent):
        pos = evt.position()
        self._last_mouse_x = pos.x()
        self._last_mouse_y = pos.y()
        self._mouse_drag = True

    def mouseMoveEvent(self, evt: QMouseEvent):
        pos = evt.position()

        if self._mouse_drag:
            self.engine.pan(
                self._factor * (pos.x() - self._last_mouse_x),
                self._factor * (pos.y() - self._last_mouse_y),
            )
            self._last_mouse_x = pos.x()
            self._last_mouse_y = pos.y()

        # update mouse coordinate display
        if pos.x() < 0 or pos.x() > self.width() or pos.y() < 0 or pos.y() > self.height():
            # noinspection PyUnresolvedReferences
            self.mouse_coords.emit("")
        else:
            x, y = self.engine.viewport_to_model(
                self._factor * pos.x(), self._factor * pos.y()
            )
            spec = self.engine.scale_spec
            decimals = max(0, math.ceil(-math.log10(1 / spec.to_px / self.engine.scale)))
            # noinspection PyUnresolvedReferences
            self.mouse_coords.emit(
                f"{x / spec.to_px:.{decimals}f}{spec.unit}, "
                f"{y / spec.to_px:.{decimals}f}{spec.unit}"
            )

    def mouseReleaseEvent(self, evt):
        self._mouse_drag = False

    def leaveEvent(self, event: QEvent) -> None:
        # noinspection PyUnresolvedReferences
        self.mouse_coords.emit("")  # type: ignore

    def wheelEvent(self, event: QWheelEvent) -> None:
        if event.source() == Qt.MouseEventSource.MouseEventSynthesizedBySystem:
            # track pad
            scroll_delta = event.pixelDelta()
            self.engine.pan(self._factor * scroll_delta.x(), self._factor * scroll_delta.y())
        else:
            # mouse wheel
            zoom_delta = event.angleDelta().y()
            pos = event.position()
            self.engine.zoom(
                zoom_delta / 500.0, self._factor * pos.x(), self._factor * pos.y()
            )

    def event(self, event: QEvent) -> bool:
        # handle pinch zoom on mac
        if (
            isinstance(event, QNativeGestureEvent)
            and event.gestureType() == Qt.NativeGestureType.ZoomNativeGesture
        ):
            self.engine.zoom(
                2.0 * event.value(),
                event.localPos().x() * self._factor,
                event.localPos().y() * self._factor,
            )
            return True

        return super().event(event)


class QtViewer(QWidget):
    """Full-featured, stand-alone viewer suitable for displaying a :class:`vpype.Document` to
    a user.
    """

    def __init__(
        self,
        document: vp.Document | None = None,
        view_mode: ViewMode = ViewMode.PREVIEW,
        show_pen_up: bool = False,
        show_points: bool = False,
        parent=None,
    ):
        super().__init__(parent)

        self._settings = QSettings()
        self._settings.beginGroup("viewer")

        self.setWindowTitle("vpype viewer")
        self.setStyleSheet(
            """
        QToolButton:pressed {
            background-color: rgba(0, 0, 0, 0.2);
        }
        """
        )

        self._viewer_widget = QtViewerWidget(parent=self)

        # setup toolbar
        self._toolbar = QToolBar()
        self._icon_size = QSize(32, 32)
        self._toolbar.setIconSize(self._icon_size)

        view_mode_grp = QActionGroup(self._toolbar)
        if _DEBUG_ENABLED:
            act = view_mode_grp.addAction("None")
            act.setCheckable(True)
            act.setChecked(view_mode == ViewMode.NONE)
            act.triggered.connect(  # type:ignore
                functools.partial(self.set_view_mode, ViewMode.NONE)
            )
        act = view_mode_grp.addAction("Outline Mode")
        act.setCheckable(True)
        act.setChecked(view_mode == ViewMode.OUTLINE)
        act.triggered.connect(  # type:ignore
            functools.partial(self.set_view_mode, ViewMode.OUTLINE)
        )
        act = view_mode_grp.addAction("Outline Mode (Colorful)")
        act.setCheckable(True)
        act.setChecked(view_mode == ViewMode.OUTLINE_COLORFUL)
        act.triggered.connect(  # type:ignore
            functools.partial(self.set_view_mode, ViewMode.OUTLINE_COLORFUL)
        )
        act = view_mode_grp.addAction("Preview Mode")
        act.setCheckable(True)
        act.setChecked(view_mode == ViewMode.PREVIEW)
        act.triggered.connect(  # type:ignore
            functools.partial(self.set_view_mode, ViewMode.PREVIEW)
        )
        self.set_view_mode(view_mode)

        # VIEW MODE
        # view modes
        view_mode_btn = QToolButton()
        view_mode_menu = QMenu(view_mode_btn)
        act = view_mode_menu.addAction("View Mode:")
        act.setEnabled(False)
        view_mode_menu.addActions(view_mode_grp.actions())
        view_mode_menu.addSeparator()
        # show pen up
        act = view_mode_menu.addAction("Show Pen-Up Trajectories")
        act.setCheckable(True)
        act.setChecked(show_pen_up)
        act.toggled.connect(self.set_show_pen_up)  # type:ignore
        self._viewer_widget.engine.show_pen_up = show_pen_up
        # show points
        act = view_mode_menu.addAction("Show Points")
        act.setCheckable(True)
        act.setChecked(show_points)
        act.toggled.connect(self.set_show_points)  # type:ignore
        self._viewer_widget.engine.show_points = show_points
        # preview mode options
        view_mode_menu.addSeparator()
        act = view_mode_menu.addAction("Preview Mode Options:")
        act.setEnabled(False)
        # pen width
        pen_width_menu = view_mode_menu.addMenu("Default Pen Width")
        act = pen_width_menu.addAction("Override")
        act.setCheckable(True)
        act.toggled.connect(self.set_override_pen_width)  # type:ignore
        pen_width_menu.addSeparator()
        act_grp = PenWidthActionGroup(0.3, parent=pen_width_menu)
        act_grp.triggered.connect(self.set_default_pen_width_mm)  # type:ignore
        pen_width_menu.addActions(act_grp.actions())
        self.set_default_pen_width_mm(0.3)

        # pen opacity
        pen_opacity_menu = view_mode_menu.addMenu("Default Pen Opacity")
        act = pen_opacity_menu.addAction("Override")
        act.setCheckable(True)
        act.toggled.connect(self.set_override_pen_opacity)  # type:ignore
        pen_opacity_menu.addSeparator()
        act_grp = PenOpacityActionGroup(0.8, parent=pen_opacity_menu)
        act_grp.triggered.connect(self.set_default_pen_opacity)  # type:ignore
        pen_opacity_menu.addActions(act_grp.actions())
        self.set_default_pen_opacity(0.8)

        # debug view
        if _DEBUG_ENABLED:
            act = view_mode_menu.addAction("Debug View")
            act.setCheckable(True)
            act.toggled.connect(self.set_debug)  # type:ignore
        # rulers
        view_mode_menu.addSeparator()
        act = view_mode_menu.addAction("Show Rulers")
        act.setCheckable(True)
        val = bool(self._settings.value("show_rulers", True))
        act.setChecked(val)
        act.toggled.connect(self.set_show_rulers)  # type:ignore
        self._viewer_widget.engine.show_rulers = val
        # units
        units_menu = view_mode_menu.addMenu("Units")
        unit_action_grp = QActionGroup(units_menu)
        unit_type = UnitType(self._settings.value("unit_type", UnitType.METRIC))
        act = unit_action_grp.addAction("Metric")
        act.setCheckable(True)
        act.setChecked(unit_type == UnitType.METRIC)
        act.setData(UnitType.METRIC)
        act = unit_action_grp.addAction("Imperial")
        act.setCheckable(True)
        act.setChecked(unit_type == UnitType.IMPERIAL)
        act.setData(UnitType.IMPERIAL)
        act = unit_action_grp.addAction("Pixel")
        act.setCheckable(True)
        act.setChecked(unit_type == UnitType.PIXELS)
        act.setData(UnitType.PIXELS)
        unit_action_grp.triggered.connect(self.set_unit_type)  # type:ignore
        units_menu.addActions(unit_action_grp.actions())
        self._viewer_widget.engine.unit_type = unit_type

        view_mode_btn.setMenu(view_mode_menu)
        view_mode_btn.setIcon(load_icon("eye-outline.svg"))
        view_mode_btn.setText("View")
        view_mode_btn.setPopupMode(QToolButton.ToolButtonPopupMode.InstantPopup)
        view_mode_btn.setStyleSheet("QToolButton::menu-indicator { image: none; }")
        self._toolbar.addWidget(view_mode_btn)

        # LAYER VISIBILITY
        self._layer_visibility_btn = QToolButton()
        self._layer_visibility_btn.setIcon(load_icon("layers-triple-outline.svg"))
        self._layer_visibility_btn.setText("Layer")
        self._layer_visibility_btn.setMenu(QMenu(self._layer_visibility_btn))
        self._layer_visibility_btn.setPopupMode(QToolButton.ToolButtonPopupMode.InstantPopup)
        self._layer_visibility_btn.setStyleSheet(
            "QToolButton::menu-indicator { image: none; }"
        )
        self._toolbar.addWidget(self._layer_visibility_btn)

        # FIT TO PAGE
        fit_act = self._toolbar.addAction(load_icon("fit-to-page-outline.svg"), "Fit")
        fit_act.triggered.connect(self._viewer_widget.engine.fit_to_viewport)  # type:ignore

        # RULER
        # TODO: not implemented yet
        # self._toolbar.addAction(load_icon("ruler-square.svg"), "Units")

        # MOUSE COORDINATES
        self._mouse_coord_lbl = QLabel("")
        self._mouse_coord_lbl.setContentsMargins(6, 6, 6, 6)
        self._mouse_coord_lbl.setAlignment(
            Qt.AlignmentFlag.AlignVCenter | Qt.AlignmentFlag.AlignRight
        )
        self._mouse_coord_lbl.setSizePolicy(
            QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Minimum
        )
        self._toolbar.addWidget(self._mouse_coord_lbl)
        # noinspection PyUnresolvedReferences
        self._viewer_widget.mouse_coords.connect(self.set_mouse_coords)  # type: ignore

        # setup horizontal layout for optional side widgets
        self._hlayout = QHBoxLayout()
        self._hlayout.setSpacing(0)
        self._hlayout.setContentsMargins(0, 0, 0, 0)
        self._hlayout.addWidget(self._viewer_widget)
        widget = QWidget()
        widget.setLayout(self._hlayout)

        # setup global vertical layout
        layout = QVBoxLayout()
        layout.setSpacing(0)
        layout.setContentsMargins(0, 0, 0, 0)
        layout.addWidget(self._toolbar)
        layout.addWidget(widget)
        self.setLayout(layout)

        if document is not None:
            self.set_document(document)

    def add_side_widget(self, widget: QWidget) -> None:
        self._hlayout.addWidget(widget)

    def set_document(self, document: vp.Document | None) -> None:
        self._viewer_widget.set_document(document)
        self._update_layer_menu()

    def _update_layer_menu(self):
        layer_menu = QMenu(self._layer_visibility_btn)
        doc = self._viewer_widget.document()
        if doc is not None:
            for layer_id in sorted(doc.layers):
                item_name = f"Layer {layer_id}"
                layer_name = doc.layers[layer_id].property(vp.METADATA_FIELD_NAME)
                if layer_name:
                    item_name += ": " + layer_name
                action = layer_menu.addAction(item_name)
                action.setCheckable(True)
                action.setChecked(True)
                # TODO: set color icon
                action.triggered.connect(
                    functools.partial(
                        self._viewer_widget.engine.toggle_layer_visibility, layer_id
                    )
                )
        self._layer_visibility_btn.setMenu(layer_menu)

    def set_mouse_coords(self, txt: str) -> None:
        self._mouse_coord_lbl.setText(txt)

    def set_view_mode(self, mode: ViewMode) -> None:
        self._viewer_widget.engine.view_mode = mode

    def set_show_pen_up(self, show_pen_up: bool) -> None:
        self._viewer_widget.engine.show_pen_up = show_pen_up

    def set_show_points(self, show_points: bool) -> None:
        self._viewer_widget.engine.show_points = show_points

    def set_default_pen_width_mm(self, value: float | QAction) -> None:
        if isinstance(value, QAction):
            value = float(value.data())
        self._viewer_widget.engine.default_pen_width = value / 25.4 * 96.0

    def set_default_pen_opacity(self, value: float | QAction) -> None:
        if isinstance(value, QAction):
            value = float(value.data())
        self._viewer_widget.engine.default_pen_opacity = value

    def set_override_pen_width(self, value: bool) -> None:
        self._viewer_widget.engine.override_pen_width = value

    def set_override_pen_opacity(self, value: bool) -> None:
        self._viewer_widget.engine.override_pen_opacity = value

    def set_debug(self, debug: bool) -> None:
        self._viewer_widget.engine.debug = debug

    def set_show_rulers(self, show_rulers: bool) -> None:
        self._viewer_widget.engine.show_rulers = show_rulers
        self._settings.setValue("show_rulers", show_rulers)

    def set_unit_type(self, sender: QAction) -> None:
        val = UnitType(sender.data())
        self._viewer_widget.engine.unit_type = val
        self._settings.setValue("unit_type", val)


def show(
    document: vp.Document,
    view_mode: ViewMode = ViewMode.PREVIEW,
    show_pen_up: bool = False,
    show_points: bool = False,
    argv=None,
) -> int:
    """Show a viewer for the provided :class:`vpype.Document` instance.

    This function returns when the user close the window.

    Args:
        document: the document to display
        view_mode: view mode
        show_pen_up: render pen-up trajectories
        show_points: render points
        argv: argument passed to Qt

    Returns:
        exit status returned by Qt
    """
    if argv is None and len(sys.argv) > 0:
        argv = [sys.argv[0]]

    app = QApplication.instance()
    if not app or not isinstance(app, QApplication):
        app = QApplication(argv)
        app.setOrganizationName("abey79")
        app.setOrganizationDomain("abey79.github.io")
        app.setApplicationName("vpype")
    app.setAttribute(Qt.ApplicationAttribute.AA_UseHighDpiPixmaps)

    widget = QtViewer(
        document, view_mode=view_mode, show_pen_up=show_pen_up, show_points=show_points
    )
    sz = app.primaryScreen().availableSize()
    widget.move(int(sz.width() * 0.05), int(sz.height() * 0.1))
    widget.resize(int(sz.width() * 0.9), int(sz.height() * 0.8))

    widget.show()

    # noinspection PyUnusedLocal
    def sigint_handler(signum, frame):
        QApplication.quit()

    with set_sigint_handler(sigint_handler):
        res = app.exec()

    return res
